/*
 * Copyright 2010, 2011, 2012 mapsforge.org
 *
 * This program is free software: you can redistribute it and/or modify it under the
 * terms of the GNU Lesser General Public License as published by the Free Software
 * Foundation, either version 3 of the License, or (at your option) any later version.
 *
 * This program is distributed in the hope that it will be useful, but WITHOUT ANY
 * WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A
 * PARTICULAR PURPOSE. See the GNU Lesser General Public License for more details.
 *
 * You should have received a copy of the GNU Lesser General Public License along with
 * this program. If not, see <http://www.gnu.org/licenses/>.
 */
package org.mapsforge.android.maps;

import java.io.File;
import java.io.FileNotFoundException;
import java.io.FileOutputStream;
import java.io.IOException;
import java.util.List;

import org.mapsforge.android.AndroidUtils;
import org.mapsforge.android.maps.inputhandling.MapMover;
import org.mapsforge.android.maps.inputhandling.TouchEventHandler;
import org.mapsforge.android.maps.inputhandling.ZoomAnimator;
import org.mapsforge.android.maps.mapgenerator.InMemoryTileCache;
import org.mapsforge.android.maps.mapgenerator.JobParameters;
import org.mapsforge.android.maps.mapgenerator.JobQueue;
import org.mapsforge.android.maps.mapgenerator.JobTheme;
import org.mapsforge.android.maps.mapgenerator.MapGenerator;
import org.mapsforge.android.maps.mapgenerator.MapGeneratorFactory;
import org.mapsforge.android.maps.mapgenerator.MapGeneratorJob;
import org.mapsforge.android.maps.mapgenerator.MapWorker;
import org.mapsforge.android.maps.mapgenerator.TileCache;
import org.mapsforge.android.maps.mapgenerator.databaserenderer.DatabaseRenderer;
import org.mapsforge.android.maps.mapgenerator.databaserenderer.ExternalRenderTheme;
import org.mapsforge.android.maps.mapgenerator.tiledownloader.TileDownloader;
import org.mapsforge.android.maps.overlay.Overlay;
import org.mapsforge.android.maps.overlay.OverlayList;
import org.mapsforge.android.maps.rendertheme.InternalRenderTheme;
import org.mapsforge.core.GeoPoint;
import org.mapsforge.core.MapPosition;
import org.mapsforge.core.MercatorProjection;
import org.mapsforge.core.Tile;
import org.mapsforge.map.reader.MapDatabase;
import org.mapsforge.map.reader.header.FileOpenResult;

import android.annotation.SuppressLint;
import android.content.Context;
import android.graphics.Bitmap;
import android.graphics.Bitmap.CompressFormat;
import android.graphics.Canvas;
import android.util.AttributeSet;
import android.view.KeyEvent;
import android.view.MotionEvent;
import android.view.ViewGroup;

/**
 * A MapView shows a map on the display of the device. It handles all user input and touch gestures to move and zoom the
 * map. This MapView also includes a scale bar and zoom controls. The {@link #getController()} method returns a
 * {@link MapController} to programmatically modify the position and zoom level of the map.
 * <p>
 * This implementation supports offline map rendering as well as downloading map images (tiles) over an Internet
 * connection. The operation mode of a MapView can be set in the constructor and changed at runtime with the
 * {@link #setMapGeneratorInternal(MapGenerator)} method. Some MapView parameters depend on the selected operation mode.
 * <p>
 * In offline rendering mode a special database file is required which contains the map data. Map files can be stored in
 * any folder. The current map file is set by calling {@link #setMapFile(File)}. To retrieve the current
 * {@link MapDatabase}, use the {@link #getMapDatabase()} method.
 * <p>
 * {@link Overlay Overlays} can be used to display geographical data such as points and ways. To draw an overlay on top
 * of the map, add it to the list returned by {@link #getOverlays()}.
 */
public class MapView extends ViewGroup
{
    /**
     * Default render theme of the MapView.
     */
    public static final InternalRenderTheme DEFAULT_RENDER_THEME = InternalRenderTheme.OSMARENDER;

    private static final float DEFAULT_TEXT_SCALE = 1;
    private static final int DEFAULT_TILE_CACHE_SIZE_IN_MEMORY = 20;

    private DebugSettings debugSettings;
    //private TileCache fileSystemTileCache;
    protected final FpsCounter fpsCounter;
    protected final FrameBuffer frameBuffer;
    private final TileCache inMemoryTileCache;
    private JobParameters jobParameters;
    private final JobQueue jobQueue;
    private final MapController mapController;
    private final MapDatabase mapDatabase;
    private File mapFile;
    private MapGenerator mapGenerator;
    private final MapMover mapMover;
    private final MapScaleBar mapScaleBar;
    private final MapViewPosition mapViewPosition;
    private final MapWorker mapWorker;
    private MapZoomControls mapZoomControls;
    protected final List<Overlay> overlays;
    private final Projection projection;
    private final TouchEventHandler touchEventHandler;
    private final ZoomAnimator zoomAnimator;

    /**
     * @param context
     *            the enclosing MapActivity instance.
     * @throws IllegalArgumentException
     *             if the context object is not an instance of {@link MapActivity}.
     */
    public MapView(Context context)
    {
        this(context, null, new DatabaseRenderer());
    }

    /**
     * @param context
     *            the enclosing MapActivity instance.
     * @param attributeSet
     *            a set of attributes.
     * @throws IllegalArgumentException
     *             if the context object is not an instance of {@link MapActivity}.
     */
    public MapView(Context context, AttributeSet attributeSet)
    {
        this(context, attributeSet, MapGeneratorFactory.createMapGenerator(attributeSet));
    }

    /**
     * @param context
     *            the enclosing MapActivity instance.
     * @param mapGenerator
     *            the MapGenerator for this MapView.
     * @throws IllegalArgumentException
     *             if the context object is not an instance of {@link MapActivity}.
     */
    public MapView(Context context, MapGenerator mapGenerator)
    {
        this(context, null, mapGenerator);
    }

    private MapView(Context context, AttributeSet attributeSet, MapGenerator mapGenerator)
    {
        super(context, attributeSet);

        if (!(context instanceof MapActivity))
        {
            throw new IllegalArgumentException("context is not an instance of MapActivity");
        }
        MapActivity mapActivity = (MapActivity) context;

        setBackgroundColor(FrameBuffer.MAP_VIEW_BACKGROUND);
        setDescendantFocusability(FOCUS_BLOCK_DESCENDANTS);
        setWillNotDraw(false);

        this.debugSettings = new DebugSettings(false, false, false);
        //this.fileSystemTileCache = new FileSystemTileCache(DEFAULT_TILE_CACHE_SIZE_FILE_SYSTEM,mapActivity.getMapViewId());
        this.fpsCounter = new FpsCounter();
        this.frameBuffer = new FrameBuffer(this);
        this.inMemoryTileCache = new InMemoryTileCache(DEFAULT_TILE_CACHE_SIZE_IN_MEMORY);
        this.jobParameters = new JobParameters(DEFAULT_RENDER_THEME, DEFAULT_TEXT_SCALE);
        this.jobQueue = new JobQueue(this);
        this.mapController = new MapController(this);
        this.mapDatabase = new MapDatabase();
        this.mapViewPosition = new MapViewPosition(this);
        this.mapScaleBar = new MapScaleBar(this);
        this.mapZoomControls = new MapZoomControls(mapActivity, this);
        this.overlays = new OverlayList(this);
        this.projection = new MapViewProjection(this);
        this.touchEventHandler = TouchEventHandler.getInstance(mapActivity, this);

        this.mapWorker = new MapWorker(this);
        this.mapWorker.start();

        this.mapMover = new MapMover(this);
        this.mapMover.start();

        this.zoomAnimator = new ZoomAnimator(this);
        this.zoomAnimator.start();

        setMapGeneratorInternal(mapGenerator);
        GeoPoint startPoint = this.mapGenerator.getStartPoint();
        if (startPoint != null)
        {
            this.mapViewPosition.setMapCenter(startPoint);
        }

        Byte startZoomLevel = this.mapGenerator.getStartZoomLevel();
        if (startZoomLevel != null)
        {
            this.mapViewPosition.setZoomLevel(startZoomLevel.byteValue());
        }

        mapActivity.registerMapView(this);
    }

    /**
     * @return the MapController for this MapView.
     */
    public MapController getController()
    {
        return this.mapController;
    }

    /**
     * @return the debug settings which are used in this MapView.
     */
    public DebugSettings getDebugSettings()
    {
        return this.debugSettings;
    }

    /**
     * @return the file system tile cache which is used in this MapView.
     */
    public TileCache getFileSystemTileCache()
    {
        return null; // BAR
    }

    /**
     * @return the FPS counter which is used in this MapView.
     */
    public FpsCounter getFpsCounter()
    {
        return this.fpsCounter;
    }

    /**
     * @return the FrameBuffer which is used in this MapView.
     */
    public FrameBuffer getFrameBuffer()
    {
        return this.frameBuffer;
    }

    /**
     * @return the in-memory tile cache which is used in this MapView.
     */
    public TileCache getInMemoryTileCache()
    {
        return this.inMemoryTileCache;
    }

    /**
     * @return the job queue which is used in this MapView.
     */
    public JobQueue getJobQueue()
    {
        return this.jobQueue;
    }

    /**
     * @return the map database which is used for reading map files.
     * @throws UnsupportedOperationException
     *             if the current MapGenerator works with an Internet connection.
     */
    public MapDatabase getMapDatabase()
    {
        if (this.mapGenerator.requiresInternetConnection())
        {
            throw new UnsupportedOperationException();
        }
        return this.mapDatabase;
    }

    /**
     * @return the currently used map file.
     * @throws UnsupportedOperationException
     *             if the current MapGenerator mode works with an Internet connection.
     */
    public File getMapFile()
    {
        if (this.mapGenerator.requiresInternetConnection())
        {
            throw new UnsupportedOperationException();
        }
        return this.mapFile;
    }

    /**
     * @return the currently used MapGenerator (may be null).
     */
    public MapGenerator getMapGenerator()
    {
        return this.mapGenerator;
    }

    /**
     * @return the MapMover which is used by this MapView.
     */
    public MapMover getMapMover()
    {
        return this.mapMover;
    }

    /**
     * @return the current position and zoom level of this MapView.
     */
    public MapViewPosition getMapPosition()
    {
        return this.mapViewPosition;
    }

    /**
     * @return the scale bar which is used in this MapView.
     */
    public MapScaleBar getMapScaleBar()
    {
        return this.mapScaleBar;
    }

    /**
     * @return the zoom controls instance which is used in this MapView.
     */
    public MapZoomControls getMapZoomControls()
    {
        return this.mapZoomControls;
    }

    /**
     * Returns a thread-safe list of overlays for this MapView. It is necessary to manually synchronize on this list
     * when iterating over it.
     * 
     * @return the overlay list.
     */
    public List<Overlay> getOverlays()
    {
        return this.overlays;
    }

    /**
     * @return the currently used projection of the map. Do not keep this object for a longer time.
     */
    public Projection getProjection()
    {
        return this.projection;
    }

    /**
     * Calls either {@link #invalidate()} or {@link #postInvalidate()}, depending on the current thread.
     */
    public void invalidateOnUiThread()
    {
        if (AndroidUtils.currentThreadIsUiThread())
        {
            invalidate();
        }
        else
        {
            postInvalidate();
        }
    }

    /**
     * @return true if the ZoomAnimator is currently running, false otherwise.
     */
    public boolean isZoomAnimatorRunning()
    {
        return this.zoomAnimator.isExecuting();
    }

    @Override
    public boolean onKeyDown(int keyCode, KeyEvent keyEvent)
    {
        return this.mapMover.onKeyDown(keyCode, keyEvent);
    }

    @Override
    public boolean onKeyUp(int keyCode, KeyEvent keyEvent)
    {
        return this.mapMover.onKeyUp(keyCode, keyEvent);
    }

    @Override
    public boolean onTouchEvent(MotionEvent motionEvent)
    {
        int action = this.touchEventHandler.getAction(motionEvent);
        this.mapZoomControls.onMapViewTouchEvent(action);
        return this.touchEventHandler.handleTouchEvent(motionEvent);
    }

    @Override
    public boolean onTrackballEvent(MotionEvent motionEvent)
    {
        return this.mapMover.onTrackballEvent(motionEvent);
    }

    /**
     * Calculates all necessary tiles and adds jobs accordingly.
     */
    public void redrawTiles()
    {
        if (this.getWidth() <= 0 || this.getHeight() <= 0)
        {
            return;
        }

        synchronized (this.overlays)
        {
            for (int i = 0, n = this.overlays.size(); i < n; ++i)
            {
                this.overlays.get(i).requestRedraw();
            }
        }

        MapPosition mapPosition = this.mapViewPosition.getMapPosition();
        if (mapPosition == null)
        {
            return;
        }

        GeoPoint geoPoint = mapPosition.geoPoint;
        double pixelLeft = MercatorProjection.longitudeToPixelX(geoPoint.getLongitude(), mapPosition.zoomLevel);
        double pixelTop = MercatorProjection.latitudeToPixelY(geoPoint.getLatitude(), mapPosition.zoomLevel);
        pixelLeft -= getWidth() >> 1;
        pixelTop -= getHeight() >> 1;

        long tileLeft = MercatorProjection.pixelXToTileX(pixelLeft, mapPosition.zoomLevel);
        long tileTop = MercatorProjection.pixelYToTileY(pixelTop, mapPosition.zoomLevel);
        long tileRight = MercatorProjection.pixelXToTileX(pixelLeft + getWidth(), mapPosition.zoomLevel);
        long tileBottom = MercatorProjection.pixelYToTileY(pixelTop + getHeight(), mapPosition.zoomLevel);

        Object cacheId;
        if (this.mapGenerator.requiresInternetConnection())
        {
            cacheId = ((TileDownloader) this.mapGenerator).getHostName();
        }
        else
        {
            cacheId = this.mapFile;
        }

        for (long tileY = tileTop; tileY <= tileBottom; ++tileY)
        {
            for (long tileX = tileLeft; tileX <= tileRight; ++tileX)
            {
                Tile tile = new Tile(tileX, tileY, mapPosition.zoomLevel);
                MapGeneratorJob mapGeneratorJob = new MapGeneratorJob(tile, cacheId, this.jobParameters, this.debugSettings);

                if (this.inMemoryTileCache.containsKey(mapGeneratorJob))
                {
                    Bitmap bitmap = this.inMemoryTileCache.get(mapGeneratorJob);
                    this.frameBuffer.drawBitmap(mapGeneratorJob.tile, bitmap);
                }
                /*
                else if (this.fileSystemTileCache.containsKey(mapGeneratorJob)) {
                	Bitmap bitmap = this.fileSystemTileCache.get(mapGeneratorJob);

                	if (bitmap != null) {
                		this.frameBuffer.drawBitmap(mapGeneratorJob.tile, bitmap);
                		this.inMemoryTileCache.put(mapGeneratorJob, bitmap);
                	} else {
                		// the image data could not be read from the cache
                		this.jobQueue.addJob(mapGeneratorJob);
                	}
                } 
                */
                else
                {
                    // cache miss
                    this.jobQueue.addJob(mapGeneratorJob);
                }
            }
        }

        if (this.mapScaleBar.isShowMapScaleBar())
        {
            this.mapScaleBar.redrawScaleBar();
        }

        invalidateOnUiThread();

        this.jobQueue.requestSchedule();
        synchronized (this.mapWorker)
        {
            this.mapWorker.notify();
        }
    }

    /**
     * Sets the visibility of the zoom controls.
     * 
     * @param showZoomControls
     *            true if the zoom controls should be visible, false otherwise.
     */
    public void setBuiltInZoomControls(boolean showZoomControls)
    {
        this.mapZoomControls.setShowMapZoomControls(showZoomControls);
    }

    /**
     * Sets the center of the MapView and triggers a redraw.
     * 
     * @param geoPoint
     *            the new center point of the map.
     */
    public void setCenter(GeoPoint geoPoint)
    {
        MapPosition mapPosition = new MapPosition(geoPoint, this.mapViewPosition.getZoomLevel());
        setCenterAndZoom(mapPosition);
    }

    /**
     * @param debugSettings
     *            the new DebugSettings for this MapView.
     */
    public void setDebugSettings(DebugSettings debugSettings)
    {
        this.debugSettings = debugSettings;
        clearAndRedrawMapView();
    }

    /**
     * Sets the map file for this MapView.
     * 
     * @param mapFile
     *            the map file.
     * @return a FileOpenResult to describe whether the operation returned successfully.
     * @throws UnsupportedOperationException
     *             if the current MapGenerator mode works with an Internet connection.
     * @throws IllegalArgumentException
     *             if the supplied mapFile is null.
     */
    public FileOpenResult setMapFile(File mapFile)
    {
        if (this.mapGenerator.requiresInternetConnection())
        {
            throw new UnsupportedOperationException();
        }
        if (mapFile == null)
        {
            throw new IllegalArgumentException("mapFile must not be null");
        }
        else
            if (mapFile.equals(this.mapFile))
            {
                // same map file as before
                return FileOpenResult.SUCCESS;
            }

        this.zoomAnimator.pause();
        this.mapWorker.pause();
        this.mapMover.pause();

        this.zoomAnimator.awaitPausing();
        this.mapMover.awaitPausing();
        this.mapWorker.awaitPausing();

        this.mapMover.stopMove();
        this.jobQueue.clear();

        this.zoomAnimator.proceed();
        this.mapWorker.proceed();
        this.mapMover.proceed();

        this.mapDatabase.closeFile();
        FileOpenResult fileOpenResult = this.mapDatabase.openFile(mapFile);
        if (fileOpenResult.isSuccess())
        {
            this.mapFile = mapFile;

            GeoPoint startPoint = this.mapGenerator.getStartPoint();
            if (startPoint != null)
            {
                this.mapViewPosition.setMapCenter(startPoint);
            }

            Byte startZoomLevel = this.mapGenerator.getStartZoomLevel();
            if (startZoomLevel != null)
            {
                this.mapViewPosition.setZoomLevel(startZoomLevel.byteValue());
            }

            clearAndRedrawMapView();
            return FileOpenResult.SUCCESS;
        }
        this.mapFile = null;
        clearAndRedrawMapView();
        return fileOpenResult;
    }

    /**
     * Sets the MapGenerator for this MapView.
     * 
     * @param mapGenerator
     *            the new MapGenerator.
     */
    public void setMapGenerator(MapGenerator mapGenerator)
    {
        if (this.mapGenerator != mapGenerator)
        {
            setMapGeneratorInternal(mapGenerator);
            clearAndRedrawMapView();
        }
    }

    /**
     * Sets the XML file which is used for rendering the map.
     * 
     * @param renderThemeFile
     *            the XML file which defines the rendering theme.
     * @throws IllegalArgumentException
     *             if the supplied internalRenderTheme is null.
     * @throws UnsupportedOperationException
     *             if the current MapGenerator does not support render themes.
     * @throws FileNotFoundException
     *             if the supplied file does not exist, is a directory or cannot be read.
     */
    public void setRenderTheme(File renderThemeFile) throws FileNotFoundException
    {
        if (renderThemeFile == null)
        {
            throw new IllegalArgumentException("render theme file must not be null");
        }
        else
            if (this.mapGenerator.requiresInternetConnection())
            {
                throw new UnsupportedOperationException();
            }

        JobTheme jobTheme = new ExternalRenderTheme(renderThemeFile);
        this.jobParameters = new JobParameters(jobTheme, this.jobParameters.textScale);
        clearAndRedrawMapView();
    }

    /**
     * Sets the internal theme which is used for rendering the map.
     * 
     * @param internalRenderTheme
     *            the internal rendering theme.
     * @throws IllegalArgumentException
     *             if the supplied internalRenderTheme is null.
     * @throws UnsupportedOperationException
     *             if the current MapGenerator does not support render themes.
     */
    public void setRenderTheme(InternalRenderTheme internalRenderTheme)
    {
        if (internalRenderTheme == null)
        {
            throw new IllegalArgumentException("render theme must not be null");
        }
        else
            if (this.mapGenerator.requiresInternetConnection())
            {
                throw new UnsupportedOperationException();
            }

        this.jobParameters = new JobParameters(internalRenderTheme, this.jobParameters.textScale);
        clearAndRedrawMapView();
    }

    /**
     * Sets the text scale for the map rendering. Has no effect in downloading mode.
     * 
     * @param textScale
     *            the new text scale for the map rendering.
     */
    public void setTextScale(float textScale)
    {
        this.jobParameters = new JobParameters(this.jobParameters.jobTheme, textScale);
        clearAndRedrawMapView();
    }

    /**
     * Takes a screenshot of the currently visible map and saves it as a compressed image. Zoom buttons, scale bar, FPS
     * counter, overlays, menus and the title bar are not included in the screenshot.
     * 
     * @param outputFile
     *            the image file. If the file already exists, it will be overwritten.
     * @param compressFormat
     *            the file format of the compressed image.
     * @param quality
     *            value from 0 (low) to 100 (high). Has no effect on some formats like PNG.
     * @return true if the image was saved successfully, false otherwise.
     * @throws IOException
     *             if an error occurs while writing the image file.
     */
    public boolean takeScreenshot(CompressFormat compressFormat, int quality, File outputFile) throws IOException
    {
        FileOutputStream outputStream = new FileOutputStream(outputFile);
        boolean success = this.frameBuffer.compress(compressFormat, quality, outputStream);
        outputStream.close();
        return success;
    }

    /**
     * Zooms in or out by the given amount of zoom levels.
     * 
     * @param zoomLevelDiff
     *            the difference to the current zoom level.
     * @param zoomStart
     *            the zoom factor at the begin of the animation.
     * @return true if the zoom level was changed, false otherwise.
     */
    public boolean zoom(byte zoomLevelDiff, float zoomStart)
    {
        float matrixScaleFactor;
        if (zoomLevelDiff > 0)
        {
            // check if zoom in is possible
            if (this.mapViewPosition.getZoomLevel() + zoomLevelDiff > getMaximumPossibleZoomLevel())
            {
                return false;
            }
            matrixScaleFactor = 1 << zoomLevelDiff;
        }
        else
            if (zoomLevelDiff < 0)
            {
                // check if zoom out is possible
                if (this.mapViewPosition.getZoomLevel() + zoomLevelDiff < this.mapZoomControls.getZoomLevelMin())
                {
                    return false;
                }
                matrixScaleFactor = 1.0f / (1 << -zoomLevelDiff);
            }
            else
            {
                // zoom level is unchanged
                matrixScaleFactor = 1;
            }

        this.mapViewPosition.setZoomLevel((byte) (this.mapViewPosition.getZoomLevel() + zoomLevelDiff));
        this.mapZoomControls.onZoomLevelChange(this.mapViewPosition.getZoomLevel());

        this.zoomAnimator.setParameters(zoomStart, matrixScaleFactor, getWidth() >> 1, getHeight() >> 1);
        this.zoomAnimator.startAnimation();
        return true;
    }

    private void setMapGeneratorInternal(MapGenerator mapGenerator)
    {
        if (mapGenerator == null)
        {
            throw new IllegalArgumentException("mapGenerator must not be null");
        }

        if (mapGenerator instanceof DatabaseRenderer)
        {
            ((DatabaseRenderer) mapGenerator).setMapDatabase(this.mapDatabase);
        }
        this.mapGenerator = mapGenerator;
        this.mapWorker.setMapGenerator(this.mapGenerator);
    }

    @Override
    protected void onDraw(Canvas canvas)
    {
        this.frameBuffer.draw(canvas);
        synchronized (this.overlays)
        {
            for (int i = 0, n = this.overlays.size(); i < n; ++i)
            {
                this.overlays.get(i).draw(canvas);
            }
        }

        if (this.mapScaleBar.isShowMapScaleBar())
        {
            this.mapScaleBar.draw(canvas);
        }

        if (this.fpsCounter.isShowFpsCounter())
        {
            this.fpsCounter.draw(canvas);
        }
    }

    @SuppressLint("WrongCall")
    @Override
    protected void onLayout(boolean changed, int left, int top, int right, int bottom)
    {
        this.mapZoomControls.onLayout(changed, left, top, right, bottom);
    }

    @Override
    protected final void onMeasure(int widthMeasureSpec, int heightMeasureSpec)
    {
        // find out how big the zoom controls should be
        this.mapZoomControls.measure(MeasureSpec.makeMeasureSpec(MeasureSpec.getSize(widthMeasureSpec), MeasureSpec.AT_MOST), MeasureSpec.makeMeasureSpec(MeasureSpec.getSize(heightMeasureSpec), MeasureSpec.AT_MOST));

        // make sure that MapView is big enough to display the zoom controls
        setMeasuredDimension(Math.max(MeasureSpec.getSize(widthMeasureSpec), this.mapZoomControls.getMeasuredWidth()), Math.max(MeasureSpec.getSize(heightMeasureSpec), this.mapZoomControls.getMeasuredHeight()));
    }

    @Override
    protected synchronized void onSizeChanged(int width, int height, int oldWidth, int oldHeight)
    {
        this.frameBuffer.destroy();

        if (width > 0 && height > 0)
        {
            this.frameBuffer.onSizeChanged();
            redrawTiles();

            synchronized (this.overlays)
            {
                for (int i = 0, n = this.overlays.size(); i < n; ++i)
                {
                    this.overlays.get(i).onSizeChanged();
                }
            }
        }
    }

    void clearAndRedrawMapView()
    {
        this.jobQueue.clear();
        this.frameBuffer.clear();
        this.inMemoryTileCache.clear(); // BAR
        redrawTiles();
    }

    void destroy()
    {
        this.overlays.clear();

        this.mapMover.interrupt();
        this.mapWorker.interrupt();
        this.zoomAnimator.interrupt();

        try
        {
            this.mapWorker.join(3000);
        }
        catch (InterruptedException e)
        {
            //restore the interrupted status
            Thread.currentThread().interrupt();
        }

        this.frameBuffer.destroy();
        this.touchEventHandler.destroy();
        this.mapScaleBar.destroy();
        this.inMemoryTileCache.destroy();
        //this.fileSystemTileCache.destroy(); // BAR
        this.mapDatabase.closeFile();
    }

    /**
     * @return the maximum possible zoom level.
     */
    byte getMaximumPossibleZoomLevel()
    {
        return (byte) Math.min(this.mapZoomControls.getZoomLevelMax(), this.mapGenerator.getZoomLevelMax());
    }

    byte getMinimumPossibleZoomLevel()
    {
        return (byte) Math.max(this.mapZoomControls.getZoomLevelMin(), this.mapGenerator.getZoomLevelMin());
    }

    /**
     * @return true if the current center position of this MapView is valid, false otherwise.
     */
    boolean hasValidCenter()
    {
        if (!this.mapViewPosition.isValid())
        {
            return false;
        }
        else
            if (!this.mapGenerator.requiresInternetConnection() && (!this.mapDatabase.hasOpenFile() || !this.mapDatabase.getMapFileInfo().boundingBox.contains(getMapPosition().getMapCenter())))
            {
                return false;
            }

        return true;
    }

    byte limitZoomLevel(byte zoom)
    {
        return (byte) Math.max(Math.min(zoom, getMaximumPossibleZoomLevel()), Math.max(zoom, getMinimumPossibleZoomLevel()));
    }

    void onPause()
    {
        this.mapWorker.pause();
        this.mapMover.pause();
        this.zoomAnimator.pause();
    }

    void onResume()
    {
        this.mapWorker.proceed();
        this.mapMover.proceed();
        this.zoomAnimator.proceed();
    }

    /**
     * Sets the center and zoom level of this MapView and triggers a redraw.
     * 
     * @param mapPosition
     *            the new map position of this MapView.
     */
    void setCenterAndZoom(MapPosition mapPosition)
    {

        if (hasValidCenter())
        {
            // calculate the distance between previous and current position
            MapPosition mapPositionOld = this.mapViewPosition.getMapPosition();

            GeoPoint geoPointOld = mapPositionOld.geoPoint;
            GeoPoint geoPointNew = mapPosition.geoPoint;
            double oldPixelX = MercatorProjection.longitudeToPixelX(geoPointOld.getLongitude(), mapPositionOld.zoomLevel);
            double newPixelX = MercatorProjection.longitudeToPixelX(geoPointNew.getLongitude(), mapPosition.zoomLevel);

            double oldPixelY = MercatorProjection.latitudeToPixelY(geoPointOld.getLatitude(), mapPositionOld.zoomLevel);
            double newPixelY = MercatorProjection.latitudeToPixelY(geoPointNew.getLatitude(), mapPosition.zoomLevel);

            float matrixTranslateX = (float) (oldPixelX - newPixelX);
            float matrixTranslateY = (float) (oldPixelY - newPixelY);
            this.frameBuffer.matrixPostTranslate(matrixTranslateX, matrixTranslateY);
        }

        this.mapViewPosition.setMapCenterAndZoomLevel(mapPosition);
        this.mapZoomControls.onZoomLevelChange(this.mapViewPosition.getZoomLevel());
        redrawTiles();
    }

}
